#!/usr/bin/env php
<?php

/**
 * Copyright Morph 2008, 2009, 2010
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @filesource
 * @category    Tool
 * @package     mCloud
 * @link        http://www.mor.ph
 * @since       v 0.1.2
 * @version     SVN: $Id:  $
 */


$help = "PROGRAM NAME\n    mCloud PHP deployment tool.\nUSAGE\n    php morph_deploy.php <parameters> [options ...]\n\nPARAMETERS\n    -u  Username/login.\n    -p  Password.\n    -f  Path to application source code (zip or diretory).\n        For SCM deployment, this will become the path to the checkout/export directory.\n    -t  Full path to temporary working folder.\n        Needed only if '-f' switch value is a directory.\nOPTIONS\n    -x  Disable SSL connection. Default is SSL enabled.\n    -r  Remove temporary directory after deployment.\n    -s  Set version name for this update.\n        Default : Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')\n    -v  Verbose mode.\n    -h  Show help.\n\n";
$deployDoneMsg = "\nDeployment Queued.\n\nFor more information on the status of this deployment, you\ncan view the Deployment Logs by clicking the 'View' icon located\non application's 'Actions' column and by clicking the 'Logs' link in the left side menu.\n\nIn this same page, you can also view your Production logs.\n\n** transaction commit **

";

/**
 * MAP deployment class for PHP.
 */
class MCloud
{
  /**
   * Setup your SCM parameters below.
   *
   * NOTES:
   * Svn
   * - You may use "export" to pull the source code from a Subversion repository
   * Git
   * - You may use "clone" or "checkout-index" to pull the source code from a Git repository
   * Mercurial
   * - You may use "clone" or "archive" to pull source code from a Mercurial repository
   * Bazaar
   * - You may use "checkout" or "export" to pull source code from a Bazaar repository
   *
   * User Accounts
   * - leave SCM username and password blank for anonymous checkout
   * - leave SCM password blank for password-less checkout
   */

  # By default we disabled the SCM support in this deployer
  const SCM_ENABLED = false;

  # Path to the remote repository
  # e.g. git://host.xz/path/to/repo.git/
  #      http://hg.mozilla.org/mobile-browser/
  const SCM_REPOSITORY = '/home/venriquez/Desktop/Q3deployments/Local/PHP/mysql/mysql';

  # SCM: subversion, git, mercurial, bazaar
  const SCM_TYPE = 'local';

  # Export: export, clone, archive, checkout-index, checkout, export
  const SCM_DEPLOY_VIA = 'export';

  # Extra SCM commands
  const SCM_EXTRA_PARAMS = "";

  # user account
  const SCM_USERNAME = "";
  const SCM_PASSWORD = "";

  /**
   * MCloud deployment settings.
   *
   * NOTE: PLEASE DO NOT CHANGE
   */ 
  const MCLOUD_HOST = "panel.coresite2.morphcloud.net";
  const MCLOUD_KEY  = "e194951d7e27dad55cfcc122b2e06a93e6191cb4";
  const MCLOUD_CONSUMER_KEY = 'VbKKw5jy5SDDTNj6APe7';
  const MCLOUD_CONSUMER_SECRET = 'mLzhPt0AydJCudap5vsq1q6OuYTPpZuqQXptQlJv';
  const MCLOUD_REQUEST_TOKEN_PATH = 'http://panel.coresite2.morphcloud.net/account/oauth/request_token';
  const MCLOUD_ACCESS_TOKEN_PATH = 'http://panel.coresite2.morphcloud.net/account/oauth/access_token';
  const MCLOUD_AUTHORIZE_PATH = 'http://panel.coresite2.morphcloud.net/account/oauth/authorize.xml';
  const MCLOUD_APP_ID = 252;
  const MCLOUD_CREATE_VERSION_PATH = 'https://panel.coresite2.morphcloud.net/applications/252/versions/api_create.xml';
  const MCLOUD_PUT_LINK_PATH = 'https://panel.coresite2.morphcloud.net/applications/252/versions/api_put_link.xml';
  const ZIPFILENAME = "code_update.tar.gz";
  const MCLOUD_TMP_DIR = "mCloud_tmp";
  const LANG = "en";
  const DS = DIRECTORY_SEPARATOR;
  const MIN_PHP = '5.2.6';

  /**
   * Array container of all CLI arguments and values.
   * @var private
   */
  private $__cliParams = array('file' => '/home/venriquez/Desktop/Q3deployments/Local/PHP/mysql/mysql');
    
  /**
   * A SimpleXML object of S3's XML authentication response.
   * @var private
   */
  private $__s3Data;

  /**
   * Path to the zipped code
   * @var private
   */
  private $__codeUpdatePath;

  /**
   * Path to temporary working folder
   * @var private
   */
    private $__mCloudTmpDir;

  /**
   * Use date as a version number
   * @var private
   */
  private $__dateVersion;

  /**
   * Error flag
   * @var private 
   */
  private $__hasErrors = false;
  
  /**
   * Error container
   * @var private
   */
  private $__errors = array('errors' => array(), 'warnings' => array());
  private $__logs = array('errors' => array(), 'warnings' => array());
  private $__win7zip = false;
  private $__winTar = false;
    
  /**
   * OAuth values
   * @var private
   */
  private $__oauth_options = array(
        'requestScheme' => Zend_Oauth::REQUEST_SCHEME_HEADER,
        'version' => '1.0',
        'consumerKey' => self::MCLOUD_CONSUMER_KEY,
        'consumerSecret' => self::MCLOUD_CONSUMER_SECRET,
        'signatureMethod' => 'HMAC-SHA1',
        'requestTokenUrl' => self::MCLOUD_REQUEST_TOKEN_PATH,
        'userAuthorizationUrl' => self::MCLOUD_AUTHORIZE_PATH,
        'accessTokenUrl' => self::MCLOUD_ACCESS_TOKEN_PATH
      );
  private $__auth_data;
  private $__request_token;
  private $__access_token;

  private $__logpath;
  
  /**
   * Constructor for MCloud class.
   *
   * @param   array   of CLI arguments
   * @return  none
   */
  function __construct(Array $cliParams) {
    $this->__cliParams = array_merge($this->__cliParams, $cliParams);
    if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
      date_default_timezone_set('UTC');
    }
    $this->__dateVersion = strftime('%Y-%m-%d %H:%M:%S');
  }
  
  public function deployNow() {
    global $deployDoneMsg;
    print $this->_('deploy-start-msg'); 

    $this->validateDeploymentEnvironment();
    if ($this->__hasErrors == false) {$this->authenticate();}
    if ($this->__hasErrors == false) {$this->setupDeploymentEnvironment();}   
    if ($this->__hasErrors == false) {$this->uploadApp();}
    if ($this->__hasErrors == false) {$this->setAppVersion();}
    
    if ($this->__hasErrors == false) {
      print $deployDoneMsg;
      $this->postDeploy();
    }
    else {
      print $this->_('deploy-app-msg-error');
      if ($this->__logpath) {
        print $this->_('deploy-log-msg-info', $this->__logpath);
      }
    }
    
    # display error messages
    #if ($this->__hasErrors == true) {
    # $this->report();
    #}
  }
  
  /**
   *  Validate PHP version, PHP extension, tmp directories, tools/utilities
   *  return none
   */
  public function validateDeploymentEnvironment() {
    print $this->_('validate-deployment-env-msg');
    
    $this->_validatePhp(); 
    if ($this->__hasErrors == false) {$this->_validateCurl();}
    if ($this->__hasErrors == false) {$this->_validateZlib();}
    if ($this->__hasErrors == false) {$this->_validateDirectories();}
  } 
  
  /**
   * Setup deployment environment
   *  e.g. checkout source codes from repo, create archive
   */
  public function setupDeploymentEnvironment() {
    $this->doScm();
    $this->createArchive();
  }
  
  /**
   * Checks if PHP zlib extension is installed 
   */
  private function _validateZlib() {
    if ($this->__hasErrors == FALSE) {
      if (!extension_loaded('zlib')) {
        print $this->_('zlib-extension-msg-error');
        $this->__hasErrors = TRUE;
        return FALSE;
      }
      return TRUE;
    }
    return FALSE;
  }  
  
  /**
   * Check the minimum requirements of PHP version 
   */
  private function _validatePhp() {
    if ($this->__hasErrors == false) {
      if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
        $this->__hasErrors = true;
        //$this->__logs['errors'][] = array('msg' => "", 'info' => "");
        print $this->_('validate-php-msg-error', self::MIN_PHP);
        return false;
      }
      return true;
    }
    return false;
  } 
  
  /**
   * Validate if cURL PHP extension is loaded. Used for transfering code from client to server
   */
  private function _validateCurl() {
    $https = false;
    if (!extension_loaded('curl')) {
      $this->__hasErrors = true;
      //$this->__logs['errors'][] = array('msg' => $this->_('curl-extension-msg-error'), 'info' => $this->_('curl-extension-msg-info'));
      print $this->_('curl-extension-msg-error');
    } else {
      # check SSL/HTTPS support           
      $curl_version = curl_version();
      foreach($curl_version['protocols'] as $protocol) {
        if (preg_match("/^\bhttps\b/i", $protocol)) {
          $https = true;
        }
      }

      # Disable cURL SSL connection 
      if ($https == false) {
        $this->__cliParams['nossl'] = true;
        //$this->__logs['warnings'][] = array('msg' => $this->_('curl-ssl-disabled-msg'));
        print $this->_('curl-ssl-disabled-msg');
      }
    }
      
    return true;
  }
  /* validation end */
  
  /* authentication */
  public function authenticate() {
    print $this->_('auth-login-msg');
    $this->_authUser();
  }
  
  private function _authUser() {
    try {  
      $consumer = new Zend_Oauth_Consumer($this->__oauth_options);
      $this->__request_token = $consumer->getRequestToken();
      if ($this->_authBasic() && $this->__auth_data) {        
        $this->__access_token = $consumer->getAccessToken(array('oauth_token' => $this->__request_token->getToken(), 'oauth_verifier' => $this->__auth_data->verifier), $this->__request_token);
        $client = $this->__access_token->getHttpClient($this->__oauth_options);
        $s3data = $this->_httpclient_request($client, self::MCLOUD_PUT_LINK_PATH, Zend_Http_Client::GET);
        $sxe = $this->_parseXml($s3data);
        if ($s3data && $sxe && !$sxe->error) {
          $this->__s3Data = $sxe;
          return TRUE;
        }
        else {
          $this->__hasErrors = TRUE;
          print $this->_('auth-s3data-msg-error');
          return FALSE;
        }
      }
    } catch(Zend_Oauth_Exception $E) {
      print $this->_('oauth-failed-error');
      $this->__hasErrors = true;
      $this->watchdog($E->getMessage());
    }
      
    return FALSE;        
  }
  
  private function _authBasic() {
    $url = $this->_getPathRedirect(self::MCLOUD_AUTHORIZE_PATH);
    # new cURL call to $url if $url is redirecting    
    $curl = curl_init();
    self::setCurlOptions($curl, 'auth-basic', array());
    curl_setopt($curl, CURLOPT_URL, $url);
    $httpResponse = curl_exec($curl);
    curl_close($curl);
    $this->__auth_data = $this->_parseXml($httpResponse);
    if (($httpResponse == FALSE) || !$this->__auth_data || $this->__auth_data->error) {
      $this->__hasErrors = TRUE;      
      print $this->_('auth-login-msg-error');
      return FALSE;
    }
    return TRUE;
  }
  
  private function _getPathRedirect($url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HEADER, TRUE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); 
    $data = curl_exec($ch);
    $info = curl_getinfo($ch);
    if ($info['http_code'] == 301 || $info['http_code'] == 302 || $info['http_code'] == 303) { 
      list($header) = explode("\r\n\r\n", $data, 2); 
      $matches = array(); 
      preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches); 
      $url = trim(array_pop($matches)); 
      $url_parsed = parse_url($url);             
    }
    curl_close($ch);
    
    return $url;
  }
  
  private function _httpclient_request($client, $url, $method, $params = array()) {
    $client->setUri($url);
    $client->setMethod($method);
    foreach ($params as $key => $value) {
      if ($method == Zend_Http_Client::POST) $client->setParameterPost($key, $value);
      else $client->setParameterGet($key, $value);
    }
    $response = $client->request();
    $result = $response->getBody();
    
    return $result;
  }
  
  private function _parseXml($xml) {
    # suppress XML errors
    libxml_use_internal_errors(TRUE);
    $data = simplexml_load_string($xml);
    $this->_logXmlError(libxml_get_errors());

    return $data;
  }

  private function _logXmlError($errors) {
    foreach ($errors as $error) {
      $this->watchdog($error->message);
    }
    libxml_clear_errors();
  }

  private function _authPanel() {}  
  private function _authStorage() {}
  private function _authScm() {}
  /* end authentication */
  
  /* reporting */
  private function report() {
    
  }
  private function _reportLogs() {}
  private function _reportErrors() {}
  private function _reportHttp() {}
  private function _reportDisplay() {}
  /* end reporting */
  
  /* utilities */
  private function uploadApp() {
    if ($this->__hasErrors == false) {
      print $this->_('upload-app-msg');

      # do upload request
      $httpHeaders = $this->getHttpHeader('upload');
      $curl = curl_init(); self::setCurlOptions($curl, 'upload', $httpHeaders);
      if (($httpResponse = curl_exec($curl)) == false || $httpResponse == '') {
        $this->http_errors($curl, 'upload');
        $this->__hasErrors = true;
        print $this->_('upload-app-msg-error');
        $this->watchdog(curl_error($curl));
      }
      curl_close($curl);

      return true;
    }
    return false;
  }

  /**
   * Sends a "set version" request to MeX so we can deploy different versions in
   * the web browser.
   *
   * @param   none
   * @return  boolean true on success; otherwise show error
   */
  private function setAppVersion() {
    try {
      if ($this->__hasErrors == false) {
        print $this->_('set-app-version-msg');
  
        # do set version request
        $versionName = $this->__dateVersion;
        if (isset($this->__cliParams['version']) && !empty($this->__cliParams['version'])) {
          $versionName = $this->__cliParams['version'];
        }
        $xml = "<?xml version='1.0' encoding='utf-8'?><version><name>$versionName</name><s3_object>". $this->__s3Data->obj_name ."</s3_object></version>";
        $client = $this->__access_token->getHttpClient($this->__oauth_options);
        $client->setUri(self::MCLOUD_CREATE_VERSION_PATH);
        $response = $client->setRawData($xml, 'application/xml')->request(Zend_Http_Client::POST);
        $result = $response->getBody();
        $sxe = $this->_parseXml($result);
        if ($result && ($sxe && $sxe->id && $sxe->name)) {
          return TRUE;
        }
        else {
          $this->__hasErrors = TRUE;
          print $this->_('set-app-version-msg-error');
          return FALSE;
        }
      }
    } catch(Zend_Oauth_Exception $E) {
      print $this->_('oauth-failed-error');
      $this->__hasErrors = true;
      $this->watchdog($E->getMessage());
    }
    return FALSE;
  }
  
  private function setCurlOptions(&$curl, $optFor, $headers) {
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);  # don't verify ssl host
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);  # don't verify ssl peer
    
    # various defaults
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_FRESH_CONNECT, true);    # don't use cache

    # verbose mode ?
    if(isset($this->__cliParams['verbose']) && $this->__cliParams['verbose'])
        curl_setopt($curl, CURLOPT_VERBOSE, true);

    switch($optFor) {            
      case 'auth-basic':              
        curl_setopt($curl, CURLOPT_POST, 1); 
        curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); //Use basic authentication
        curl_setopt($curl, CURLOPT_USERPWD, $this->__cliParams['username'] .':'. $this->__cliParams['password']);
        $params = implode('&', array('oauth_token='. $this->__request_token->getToken(), 'authorize=1'));
        curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
        curl_setopt($curl, CURLOPT_HEADER, FALSE);
        break;

      case 'upload':
        $fd = fopen($this->__codeUpdatePath, "rb");
        $fs = filesize($this->__codeUpdatePath);
        if (isset($this->__s3Data->port) && !preg_match('/\b443\b/i', $this->__s3Data->port)) {          
          curl_setopt($curl, CURLOPT_PORT, $this->__s3Data->port);
        }
        $url = trim($this->__s3Data->host).trim($this->__s3Data->path);
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_PUT, true);
        curl_setopt($curl, CURLOPT_HEADER, true);
        curl_setopt($curl, CURLOPT_INFILE, $fd);
        curl_setopt($curl, CURLOPT_INFILESIZE, $fs);
        curl_setopt($curl, CURLOPT_NOPROGRESS, false);  # show a detailed upload status                    
        break;
            
      default:
        $this->__hasErrors = true;
        print $this->_('command-http-request-msg-error', $optFor);
        break;
    }
  }
    
  private function getSysInfo($code) {
    switch($code) {
      case 'os':
        $info = substr(PHP_OS, 0, 3);
        break;
      case 'php':
        $info = phpversion();
        break;
      case 'curl':
        $info = "";
        break;
    }
    
    return $info;
  }
  
  private function errorCode() {}
  private function errorHttp() {}
  
  public function _hasErrors($errorFlag, $errorLogs) {
    $this->__hasErrors = $errorFlag;
    $this->__logs = $errorLogs;
  }
  
  /* end utilities */
  
  /**
   * Post deployment tasks e.g. clean, etc.
   *
   * @param   none
   * @return  none
   */
  private function postDeploy() {
    # -r
    if (isset($this->__cliParams['clean']) && $this->__cliParams['clean'] === true) {
      $this->clean($this->__codeUpdatePath);
      if (isset($this->__cliParams['tmp'])) {
        $this->clean($this->__mCloudTmpDir, true);
      }
    }
  }

  /**
   * Returns appropriate HTTP header for a specific HTTP requests.
   *
   * @param   string  the type of header to set
   * @return  array   of HTTP headers
   */
  protected function getHttpHeader($headerFor) {
    switch($headerFor) {            
      case 'upload':
        # cURL will auto-populate the Content-Length value
        $headers = array (
          "Accept: */*", "Expect:",
          "x-amz-date: ".$this->__s3Data->header->attributes()->{'x-amz-date'},
          "Authorization: ".$this->__s3Data->header->attributes()->Authorization,
          "Content-type: ".$this->__s3Data->header->attributes()->{'Content-type'},
          "AWS-Version: ".$this->__s3Data->header->attributes()->{'AWS-Version'},
          "Host: ".$this->__s3Data->header->attributes()->Host,
        );
        break;
        
      default :
        //$errorLogs['errors'][] = array('msg' => $this->_('get-httpheader-msg-error', $headerFor));
        //$this->_hasErrors(true, $errorLogs);
        $this->__hasErrors = true;
        print $this->_('get-httpheader-msg-error', $headerFor);
        break;
    }
     return $headers;
  }

  /**
   * Checks http return codes.
   *
   * @param   handle  $curl handle
   * @param   string  name of the current deployment task
   * @return  none
   */
  private function http_errors($curl, $errorFor) {
    $httpResponseInfo = curl_getinfo($curl);
    switch($httpResponseInfo['http_code']) {
      case 200:
        print "Done\n";
        break;
      case 500:
      case 501:
      case 502:
      case 503:
      case 504:
      case 505:
        $this->__hasErrors = true;
        print $this->_('http-505-msg-error', $httpResponseInfo['http_code']);
        break;
      case 407:
        $this->__hasErrors = true;
        print $this->_('http-407-msg-error');
        break;
      case 401:
      case 403:
        $this->__hasErrors = true;
        switch($errorFor) {
          case 'auth':
            print $this->_('http-403-auth-msg-error');
            break;
          case 'upload':
            print $this->_('http-403-upload-msg-error');
            break;
          case 'deploy':
            print $this->_('http-403-deploy-msg-error');
            break;
          case 'version':
            print $this->_('http-403-version-msg-error');
            break;
          default:
            break;
        }
        break;
      default:
        $this->__hasErrors = true;
        print $this->_('http-msg-error', $httpResponseInfo['http_code']);
        break;
      }
  }
  
  /**
   * Process file/directory for deployment.
   *
   * @param   string  path to $file (dir/zip)
   * @return  boolean true; otherwise display error.
   */
  private function _validateDirectories() {
    $file = $this->__cliParams['file'];
    # @todo remove realpath
    $tmp_dir = (empty($this->__cliParams['tmp'])) ? null : realpath($this->__cliParams['tmp']);
    $this->__mCloudTmpDir = $tmp_dir.self::DS.self::MCLOUD_TMP_DIR;

    # Make sure the file or directory exist.
    if (!file_exists($file)) {
      //$errorLogs['errors'][] = array('msg' => $this->_('validate-directory-msg-error'));
      //$this->_hasErrors(true, $errorLogs);
      $this->__hasErrors = true;
      print $this->_('validate-directory-msg-error');
    }

    switch(is_dir($file)) {
      case false:
        $this->__codeUpdatePath = $file;
        #break;
      default:
        $source_path = base64_encode($file);
        $script_path = base64_encode(__FILE__);
        $tmp_path = base64_encode($tmp_dir);

        # don't run this script inside the source directory
        /*
         @TODO 
        if (preg_match("/$source_path/i", $tmp_path) || preg_match("/$source_path/i", $script_path) )
        $errorLogs['errors'][] = array('msg' => $this->_('validate-directory-conflict-msg-error'));
        $this->_hasErrors(true, $errorLogs);
        */  
        
        # check tmp directory
        # @todo create tmp if not exist in /tmp
        if (empty($tmp_dir) && is_dir($file)) {
          //$errorLogs['errors'][] = array('msg' => $this->_('validate-directory-tmp-missing-msg-error'));
          //$this->_hasErrors(true, $errorLogs);
          $this->__hasErrors = true;
          print $this->_('validate-directory-tmp-missing-msg-error');
        }
        
        # check tmp directory permissions
        if (!is_dir($tmp_dir) && !is_writable($tmp_dir) && !is_readable($tmp_dir)) {
          //$errorLogs['errors'][] = array('msg' => $this->_('validate-directory-tmp-invalid-msg-error'));
          //$this->_hasErrors(true, $errorLogs);
          $this->__hasErrors = true;
          print $this->_('validate-directory-tmp-invalid-msg-error');
        }
        
        # check code directory permissions
        if (!is_dir($file) && !is_writable($file) && !is_readable($file)) {
          //$errorLogs['errors'][] = array('msg' => $this->_('validate-directory-tmp-unreadable-msg-error'));
          //$this->_hasErrors(true, $errorLogs);
          $this->__hasErrors = true;
          print $this->_('validate-directory-tmp-unreadable-msg-error');
        }
          
        $this->__codeUpdatePath = $file;
        break;
    }
    return true;
  }

  /**
   * Creates an archive of the source code.
   *
   * @param   string  $path to directory
   * @param   string  Path to temporary working folder
   * @return  boolean true; otherwise show error.
   */
  private function createArchive() {
    if (!empty($this->__codeUpdatePath) && is_dir($this->__codeUpdatePath)) {
      $tmp_path = $this->__mCloudTmpDir;
      $tar_name = $tmp_path.self::DS.self::ZIPFILENAME;
      $tar_name = "$tar_name";
      # clean-up, delete existing
      if(!file_exists($tmp_path)) mkdir($tmp_path); else self::clean($tar_name);

      $obj = new Archive_Tar($tar_name, 'gz');
      $target = $this->__cliParams['file'];
      $files = array($target);
      if (($obj->createModify($files, './', $target))) {
        $this->__codeUpdatePath = $tar_name;
        return TRUE;
      }
      else {
        $this->__hasErrors = true;
        print $this->_('archive-tar-msg-error', $path);
        return FALSE;
      }
    }
  }

  /**
   * Performs SCM export/checkout.
   *
   * @param   none
   * @return  true/false.
   */
  private function doScm() {
    if (self::SCM_ENABLED == true) {
      $this->scmValidator();
      switch(self::SCM_TYPE) {
        case 'bzr':
          $this->bazaar();
          break;
        case 'mercurial':
          $this->mercurial();
          break;
        case 'git':
          $this->git();
          break;
        case 'subversion':
          $this->svn();
        default:
          break;
      }
    }
    return true;
  }

  /**
   * Validates SCM parameters
   * @return  boolean true; otherwise show error and exit.
   */
  private function scmValidator() {
    if (self::SCM_ENABLED) {
      if (self::SCM_REPOSITORY == "" || self::SCM_TYPE == "" || self::SCM_DEPLOY_VIA == "") {
        $this->__hasErrors = true;
        print $this->_('vcs-msg-error');
      }
    }
  }

  /**
   * Subversion deployment method.
   *
   * NOTE: SVN is set to "non-interactive" mode. So you need to explicitly input
   *       the username, password and etc.
   *
   * @param   none
   * @return  boolean true; otherwise show error
   */
  private function svn() {
    print $this->_('vcs-svn-msg');
    $deployVia = self::SCM_DEPLOY_VIA;
    $scmRepo = self::SCM_REPOSITORY;
    $scmUsername = self::SCM_USERNAME;
    $scmPassword = self::SCM_PASSWORD;

    # Re-use the -f switch value as the checkout/export directory
    $checkOutPath = $this->__cliParams['file'];

    # check for anonymous or password-less SVN checkout/export
    if (empty($scmUsername) || empty($scmPassword)) {
      if (empty($scmPassword) && !empty($scmUsername)) {
        $svn_account = "--username $scmUsername";
      } else {
        # anonymous checkout;
        # username is empty or username and password are empty.
        $svn_account = "";
      }
    } else {
      $svn_account = "--username $scmUsername --password $scmPassword";
    }

    # format svn cmd
    $svn_cmd = "svn  --force $deployVia $scmRepo \"$checkOutPath\" $svn_account --non-interactive";

    # do svn checkout/export.
    system($svn_cmd, $retval);
    if ($retval !== 0) {
      $this->__hasErrors = true;
      print $this->_('vcs-svn-msg-error');
    }

    return true;
  }

  /**
   * Git deployment method.
   *
   * @param none
   * @returns boolean   true; otherwise error
   */
  private function git() {
    print $this->_('vcs-git-msg');
    $deployVia = self::SCM_DEPLOY_VIA;
    $scmRepo   = self::SCM_REPOSITORY;

    # git-clone to an existing directory is not allowed.
    # So we'll set a random name for the checkout directory
    $this->__cliParams['file'] = $this->__cliParams['file'].self::DS."gitrepo_".$this->dateToString();

    # Re-use "-f" switch value as the checkout/export directory
    $checkOutPath = $this->__cliParams['file'];

    switch($deployVia) {
      case 'checkout-index':
        # Make sure that the repo is pointing to an Git cloned directory
        if (!is_dir($scmRepo)) {
          $this->__hasErrors = true;
          print $this->_('vcs-git-export-dir-msg-error', $scmRepo);
        }

        # We can't do checkout-index unless we're in the current cloned repo directory.
        # We change directory to the cloned repo and do checkout-index command.
        chdir($scmRepo);

        # The trailing directory separator is important so that Git
        # will know that we're pointing to a directory. @see git-checkout-index manual
        $checkOutPath .= self::DS;

        # format Git cmd
        $git_cmd = "git $deployVia --prefix=\"$checkOutPath\" -a -f ";
        break;
      case 'clone':
        # format Git cmd
        $git_cmd = "git $deployVia $scmRepo \"$checkOutPath\"; rm -rf \"$checkOutPath\"/.git ";
      default:
        break;
    }

    # do Git checkout/export.
    system($git_cmd, $retval);
    if ($retval !== 0) {
      $this->__hasErrors = true;
      print $this->_('vcs-git-msg-error');
    }

    return true;
  }

  /**
   * Mercurial deployment method.
   *
   * @param none
   * @returns boolean   true; otherwise error
   */
  private function mercurial() {
    print $this->_('vcs-mercurial-msg');
    $deployVia = self::SCM_DEPLOY_VIA;
    $scmRepo   = self::SCM_REPOSITORY;

    # hg-clone to an existing directory is not allowed.
    # So we'll set a random name for the checkout directory
    $this->__cliParams['file'] = $this->__cliParams['file'].self::DS."hgrepo_".$this->dateToString();

    # Re-use "-f" switch value as the checkout/export directory
    $checkOutPath = $this->__cliParams['file'];

    switch($deployVia) {
      case 'archive':
        # Make sure that the repo is pointing to a Hg cloned directory
        # TODO: Validate an hg repo
        if (!is_dir($scmRepo)) {
          $this->__hasErrors = true;
          print $this->_('vcs-mercurial-export-dir-msg-error', $scmRepo);
        }

        # We can't do source archive unless we're in the current cloned repo directory.
        # We change directory to the cloned repo and do archive command.
        chdir($scmRepo);

        # format Hg cmd
        $hg_cmd = "hg -v $deployVia \"$checkOutPath\"";
        break;
      case 'clone':
        $hg_cmd = "hg -v $deployVia $scmRepo \"$checkOutPath\"; rm -rf \"$checkOutPath\"/.hg ";
      default :
        break;
    }

    # do Hg clone/archive.
    system($hg_cmd, $retval);
    if ($retval !== 0) {
      $this->__hasErrors = true;
      print $this->_('vcs-mercurial-msg-error');
    }

    return true;
  }

  /**
   * Bazaar deployment method.
   *
   * @param none
   * @returns boolean   true; otherwise error
   */
  private function bazaar() {
    print $this->_('vcs-bzr-msg');
    $deployVia = self::SCM_DEPLOY_VIA;
    $scmRepo   = self::SCM_REPOSITORY;

    # bzr checkout/export to an existing directory is not allowed.
    # So we'll set a random name for the checkout directory
    $this->__cliParams['file'] = $this->__cliParams['file'].self::DS."bzrrepo_".$this->dateToString();

    # Re-use "-f" switch value as the checkout/export directory
    $checkOutPath = $this->__cliParams['file'];

    switch($deployVia) {
      case 'export':
        # Make sure that the repo is pointing to a Bazaar repo
        if (!is_dir($scmRepo)) {
          $this->__hasErrors = true;
          print $this->_('vcs-bzr-export-dir-msg-error', $scmRepo);
        }

        # We can't do source export unless we're in the current Bazaar repo directory.
        # We change directory to the repo and do export command.
        chdir($scmRepo);

        # format Bzr cmd
        $bzr_cmd = "bzr $deployVia \"$checkOutPath\"";
        break;
      case 'checkout':
        $bzr_cmd = "bzr $deployVia $scmRepo \"$checkOutPath\"; rm -rf \"$checkOutPath\"/.bzr ";
        break;
      default :
        break;
    }

    # do Bzr checkout/export.
    system($bzr_cmd, $retval);
    if ($retval !== 0) {
      $this->__hasErrors = true;
      print $this->_('vcs-bzr-msg-error', $deployVia);
    }

    return true;
  }

  /**
   * Converts a date format into a string
   */
  private function dateToString() {
    $pattern[0] = '/ /'; $pattern[1] = '/:/';
    $replace[1] = '-'; $replace[0] = '-';

    return preg_replace($pattern, $replace, $this->__dateVersion);
  }

  /**
   * Clean-up previous deploys
   *
   * @param   string  $path to file
   * @param   boolean is $path a directory ?
   * @return  boolean Returns always true;
   */
  private static function clean($path, $isDir = false){
    if(file_exists($path)) {
      if($isDir) rmdir($path);
      if(!$isDir) unlink($path);
    }
    return true;
  }

  /**
   * A utility to do basic processing of the CLI params.
   *
   * @param   array   of raw CLI params
   * @return  array   of formatted CLI params
   */
  public static function processParams() {
    global $help;

    $options = MCloud::_getopt();

    if (sizeof($options) <= 0) { print $help; exit; }
    foreach(array_keys($options) as $o) switch($o) {
      # required parameters
      case 'u':
        $cliParams['username'] = $options[$o];
        break;
      case 'p':
        $cliParams['password'] = $options[$o];
        break;
      case 'f':
        $options[$o] = trim($options[$o]);
        $cliParams['file'] = rtrim($options[$o], '/');
        break;
      case 't':
        $options[$o] = trim($options[$o]);
        $cliParams['tmp'] = rtrim($options[$o], '/');
        break;
          
      # optional parameters
      case 'x':
        $cliParams['nossl'] = true;
        break;
      case 'r':
        $cliParams['clean'] = true;
        break;
      case 'v':
        $cliParams['verbose'] = true;
        break;
      case 's':
        $cliParams['version'] = $options[$o];
        break;
      case 'h':
        print $help;
        exit(0);
      default:
        print $help;
        print MCloud::_('command-argument-msg-error', $o);
        exit(1);
    }
        
    // Ask for user name if not present
    if (!isset($cliParams['username']) || empty($cliParams['username'])) {
      $name = self::getCliInput('auth-panel-username-msg');
      $cliParams['username'] = $name;
    }
    
    /* ask for panel password */
    if (!isset($cliParams['password']) || empty($cliParams['password'])) {
      $password = self::getPassword();
      $cliParams['password'] = $password;
    }
        
    // set OS tmp dir as default value for -t param if not given
    if (!isset($cliParams['tmp']) || !$cliParams['tmp']) {
      $cliParams['tmp'] = sys_get_temp_dir();  
    }
        
    return $cliParams;
  }

  public static function watchdog($message) {
    error_log("\r\n". date('M d Y H:i:s') ."\r\n". $message ."\r\n", 3, self::getLogpath());
  }
  
  public static function getLogpath() {
    $path = dirname(__FILE__);
    return $path . self::DS .'mCloud-'. self::SCM_TYPE .'-deploy-'. self::MCLOUD_APP_ID . date('-Y-m-d', time()) .'.log';
  }

  /**
   * Get an input from the CLI.
   * 
   * @return string STDIN value without white-space
   */
  private function getCliInput($msg = '') {
    $value = '';
    do {
      if ($msg) fwrite(STDOUT, MCloud::_($msg));
      $handle = fopen ("php://stdin","r");
      $value = fgets($handle);
    } while ( trim($value) == '' );
    
    return trim($value);
  }

  /**
   * Get password.
   *
   * @return string STDIN value without white-space
   */
  private function getPassword() {
    $value = '';
    do {
      fwrite(STDOUT, MCloud::_('auth-panel-password-msg'));
      $handle = fopen ("php://stdin","r");
      $value = fgets($handle);
    } while ( trim($value) == '' );

    return trim($value);
  }
    
  /**
   * A basic getopt implementation for both *NIX and Win* Systems.
   * Native getopt on Win is only available in 5.3.x
   *
   * getopt referrence :
   *  $shortOpts = "xrhvu:p:c:f:s::t::";
   *
   * @return array of cli options
   */
  public static function _getopt() {
    $argv = $GLOBALS['argv'];

    # remove script filename
    unset($argv[0]);

    foreach($argv as $k => $v) {
      if (preg_match("/^-/i", $v)) {
        if (isset($argv[$k+1]) && !preg_match("/^-/i", trim($argv[$k+1]))) {
          $options[$v{1}] = $argv[$k+1];
        } else {
          $options[$v{1}] = "";
        }
      }
    }
    return $options;
  }

  /**
   * localization for errors, logs, messages
   */
  private function _($id, $params = "") {
    switch($id) {
      case 'archive-tar-msg-error':
        $text = sprintf("[ERROR] Unable to create Tar archive to ", $params);
        break;
      case 'archive-gzip-msg-error':
        $text = sprintf("[ERROR] Unable to create Gzip archive to ", $params);
        break;
      case 'http-msg-error':
        $text = sprintf("[ERROR] Server returned %s.", $params);
        break; 
      case 'http-505-msg-error':
        $text = sprintf("[ERROR] Server error %s.", $params);
        break;
      case 'http-407-msg-error':
        $text = "[ERROR] Proxy error..";
        break;
      case 'http-403-auth-msg-error':
        $text = "[ERROR] Authentication failed.";
        break;
      case 'http-403-upload-msg-error':
        $text = "[ERROR] Authentication failed when uploading file to cloud.";
        break;
      case 'http-403-deploy-msg-error':
        $text = "[ERROR] Authentication failed when deploying your application.";
        break;
      case 'http-403-version-msg-error':
        $text = "[ERROR] Authentication failed when setting deployment version.";
        break;
      case 'command-http-request-msg-error':
        $text = sprintf("[ERROR] Invalid HTTP request : %s", $params);
        break;
      case 'command-argument-msg-error':
        $text =  sprintf("[ERROR] Invalid arguments found [%s].", $params);
        break;
      case 'vcs-msg-error':
        $text = "[ERROR] Missing SCM parameters.";
        break;
      case 'vcs-bzr-msg':
        $text = "Getting source code from Bazaar repository..."; 
        break;
      case 'vcs-bzr-msg-error':
        $text = sprintf("[ERROR] Unable to do Bazaar %s", $params);
        break;
      case 'vcs-bzr-export-dir-msg-error':
        $text = sprintf("[ERROR] The specified path is not a directory %s", $params);
        break;
      case 'vcs-git-msg':
        $text = "Getting source code from Git repository..." ;
        break;
      case 'vcs-git-msg-error':
        $text = sprintf("[ERROR] Unable to do Git %s", $params);
        break;
      case 'vcs-git-export-dir-msg-error':
        $text = sprintf("[ERROR] The specified path is not a directory %s", $params);
        break;        
      case 'vcs-mercurial-msg':
        $text = "Getting source code from Mercurial repository...";
        break;  
      case 'vcs-mercurial-msg-error':
        $text = sprintf("[ERROR] Unable to do Mercurial %s", $params);
        break;
      case 'vcs-mercurial-export-dir-msg-error':
        $text = sprintf("[ERROR] The specified path is not a directory %s", $params);
        break;
      case 'vcs-svn-msg':
        $text = "Getting source code from Subversion repository...";
        break;
      case 'vcs-svn-msg-error':
        $text = sprintf("[ERROR] Unable to do Subversion %s", $params);
        break;              
      case 'vcs-svn-export-dir-msg-error':
        $text = sprintf("[ERROR] The specified path is not a directory %s", $params);
        break;        
      case 'validate-php-msg-error':
        $text = sprintf("[ERROR] The minimum required PHP version is %s", $params);
        break;
      case 'validate-deployment-env-msg':
        $text = "Setting-up deployment environment...";
        break;
      case 'validate-directory-msg-error':
        $text = "The source directory could not be found.";
        break;
      case 'validate-directory-conflict-msg-error':
        $text = "[ERROR] The deployment script and temporary working folder should not be inside the source directory.";
        break;
      case 'validate-directory-tmp-missing-msg-error':
        $text = "[ERROR] Missing temporary working directory.";
        break;
      case 'validate-directory-tmp-invalid-msg-error':
        $text = "[ERROR] Temporary directory is invalid. Please check directory permission.";
        break;
      case 'validate-directory-tmp-unreadable-msg-error':
        $text = "[ERROR] The temporary or working directory is unreadable.";
        break;
      case 'auth-login-msg':
        $text = "Authenticating... ";
        break;
      case 'auth-login-msg-error':
        $text = "[ERROR] Authentication failed.";
        break;
      case 'auth-s3data-msg-error':
         $text = "[ERROR] Could not get the server storage connection data.";
         break;
      case 'auth-panel-username-msg':
        $text = "Please enter username ";
        break;
      case 'auth-panel-password-msg-error':
        $text = "[ERROR] Please enter your password";
        break;
      case 'auth-panel-password-msg':
        $text = "Please enter your password";
        break;
      case 'preprocess-set-env-msg':
        $text = "Setting-up deployment environment...";
        break;
      case 'upload-app-msg':
        $text =  "Uploading code to cloud...";
        break;
      case 'upload-app-msg-error':
        $text =  "[ERROR] Could not upload code to cloud.";        
        break;
      case 'deploy-start-msg':
        $text = "Starting deployment process...";
        break;
      case 'deploy-app-msg':
        $text = "Deploying application... ";
        break;
      case 'deploy-app-msg-error':
        $text = "[ERROR] Unable to deploy application. Please try again.\n";
        break;
      case 'deploy-log-msg-info':
        $text = sprintf("Please see the log file for additional information: '%s'\n", $params);
        break;
      case 'set-app-version-msg':
        $text = "Creating new application version...";
        break;
      case 'set-app-version-msg-error':
        $text = "[ERROR] Unable to create new version.";
        break;
      case 'curl-extension-msg-error' :
        $text = "[ERROR] CURL PHP extension not loaded.";
        break;
      case 'curl-extension-msg-info':
        $text = "[ERROR] Please install CURL PHP extension before deploying your application.";
        break;
      case 'curl-ssl-disabled-msg':
        $text = "[WARNING] CURL SSL support is disabled.";
        break;
      case 'zlib-extension-msg-error':
        $text = "[ERROR] PHP zlib extension not installed. Refer to http://php.net/manual/en/book.zlib.php for installation instructions.";
        break;
      case 'oauth-failed-error':
        $text = "[ERROR] Bad OAuth Request.";
        break;
      case 'get-httpheader-msg-error':
        $text = sprintf("[ERROR] Could not set HTTP headers %s", $params);
        break;
        /*
      default:
        #$text = sprintf("Error message for '%s' could not be found.", $id);
        $text = sprintf("[ERROR] translation missing: en, applications, deployer, php, error_msg_not_found '%s'", $id);
        break;
        */
    }   
    $text = "\n$text\n";
    
    return $text;
  }

}

/**
 * Log errors.
 *
 */
function mcloud_error_handler($error_level, $message, $filename, $line, $context = null) {
  if ($error_level & error_reporting()) {
    $types = array(
      E_ERROR => 'Error',
      E_WARNING => 'Warning',
      E_PARSE => 'Parse error',
      E_NOTICE => 'Notice',
      E_CORE_ERROR => 'Core error',
      E_CORE_WARNING => 'Core warning',
      E_COMPILE_ERROR => 'Compile error',
      E_COMPILE_WARNING => 'Compile warning',
      E_USER_ERROR => 'User error',
      E_USER_WARNING => 'User warning',
      E_USER_NOTICE => 'User notice',
      E_STRICT => 'Strict warning',
      E_RECOVERABLE_ERROR => 'Recoverable fatal error',
    );
    // E_DEPRECATED and E_USER_DEPRECATED were added in PHP 5.3.0.
    if (defined('E_DEPRECATED')) {
      $types[E_DEPRECATED] = 'Deprecated function';
      $types[E_USER_DEPRECATED] = 'User deprecated function';
    }

    $entry = $types[$error_level] .': '. $message .' in '. $filename .' on line '. $line .'.';    
    
    MCloud::watchdog($entry);
  }
}

function mcloud_shutdown() {
  $last_error = error_get_last();
  if ($last_error['type'] === E_ERROR) {
    mcloud_error_handler(E_ERROR, $last_error['message'], $last_error['file'], $last_error['line']);

    $msg = "\n". 'Error: '. $last_error['message'] .' in line '. $last_error['line'] .".\n\n";
    print "\n[ERROR] ". sprintf("Deploy script terminated abnormally due to an unrecoverable error.%s", $msg);        
  }
}

function validateZendFramework() {
  $path = FALSE;
  $include_paths = explode(PATH_SEPARATOR, get_include_path());
  foreach ($include_paths as $include_path) {
    if (is_dir($include_path .'/Zend')) {
      $path = $include_path .'/Zend';
    }
  }
  return $path;
}

// Set error handler.
set_error_handler('mcloud_error_handler');
register_shutdown_function('mcloud_shutdown');

if (!validateZendFramework()) {
  print "\n[ERROR] Zend Framework was not found in PHP's 'include_path' directive. Download it from http://framework.zend.com and check its INSTALL file for installation instructions.\n\n";
  exit;
}
else {
  require_once 'Zend/Oauth/Consumer.php';
  require_once 'Archive_Tar.php';
}

/******************************************************************************
 *                          MAIN ENTRY POINT                                  *
 ******************************************************************************/

# process CLI arguments
$cliParams = MCloud::processParams();

# Start deployment process
$mCloud = new MCloud($cliParams);
$mCloud->deployNow();

if (file_exists($path = MCloud::getLogpath())) {
  print sprintf("Please see the log file for additional information:\n%s\n\n", $path);
}

?>
